from __future__ import annotations

import sys
import threading
import subprocess
from typing import Callable, Optional


class LocalShellClient:
    """Spawn a local interactive shell (PowerShell on Windows, bash otherwise).

    Minimal line-based interface compatible with SessionTab expectations.
    """

    def __init__(self, on_data: Optional[Callable[[str], None]] = None) -> None:
        self._proc: Optional[subprocess.Popen] = None
        self._on_data = on_data
        self.is_connected = False
        self._reader: Optional[threading.Thread] = None

    def connect(self) -> None:
        if self.is_connected:
            return
        if sys.platform.startswith('win'):
            # Start PowerShell without profile/logo for predictable output
            cmd = [
                "powershell.exe",
                "-NoLogo",
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
            ]
        else:
            cmd = ["/bin/bash", "-i"]
        # Use binary mode to capture prompts and partial lines immediately
        # (PowerShell often writes prompts without trailing newlines)
        self._proc = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            bufsize=0,  # unbuffered
        )
        self.is_connected = True
        self._reader = threading.Thread(target=self._read_loop, daemon=True)
        self._reader.start()
        # Try to enforce UTF-8 I/O in the shell to avoid mojibake (best-effort)
        try:
            if sys.platform.startswith('win'):
                # Set both Input/Output encoding to UTF-8 in PowerShell
                init = (
                    "$enc = [System.Text.UTF8Encoding]::new(); "
                    "[Console]::OutputEncoding = $enc; "
                    "[Console]::InputEncoding = $enc"
                )
                self.send_line(init, "\r\n")
        except Exception:
            pass

    def _read_loop(self) -> None:
        assert self._proc is not None
        out = self._proc.stdout
        if out is None:
            self.is_connected = False
            return
        try:
            while True:
                chunk = out.read(1024)
                if not chunk:
                    break
                # Decode bytes defensively (prefer UTF-8)
                if isinstance(chunk, bytes):
                    try:
                        text = chunk.decode('utf-8', errors='ignore')
                    except Exception:
                        text = chunk.decode(errors='ignore')
                else:
                    text = chunk  # already str (unlikely in our setup)
                if self._on_data and text:
                    self._on_data(text)
        except Exception:
            pass
        finally:
            self.is_connected = False

    def send_line(self, text: str, line_end: str = "\n") -> None:
        if not self._proc or not self._proc.stdin:
            return
        try:
            data = (text + line_end).encode('utf-8', errors='ignore')
            self._proc.stdin.write(data)
            self._proc.stdin.flush()
        except Exception:
            self.is_connected = False

    def disconnect(self) -> None:
        if not self._proc:
            return
        try:
            if self._proc.stdin:
                try:
                    # polite exit
                    exit_cmd = ("exit\r\n" if sys.platform.startswith('win') else "exit\n")
                    self._proc.stdin.write(exit_cmd.encode('utf-8', errors='ignore'))
                    self._proc.stdin.flush()
                except Exception:
                    pass
            self._proc.terminate()
        except Exception:
            pass
        finally:
            self.is_connected = False
            self._proc = None
